import { get } from 'lodash-es';
import {
  DecoderFunction,
  array,
  fields,
  literal,
  optional,
  record,
  string,
  union,
} from 'typescript-json-decoder';
import {
  ViewEventBinding,
  GeneralModel,
  ViewModelBinding,
  ViewTemplate,
} from '../runtime';

interface ModelBinding {
  type: 'expr' | 'sync';
  ref: string;
}

interface ViewTemplateNode {
  type: string;
  name: string;
  props?: Array<{
    name: string;
    bind: ModelBinding;
  }>;
  layout?: {
    [key: string]: any;
  } | null;
  on?: Array<{
    name: string;
    code: string;
  }>;
  elements?: Array<ViewTemplateNode>;
}

type ViewTemplateSchema = ViewTemplateNode;

const idCounter = {
  count: 0,
};

const validateViewTemplateData: DecoderFunction<ViewTemplateSchema> = record({
  type: string,
  name: fields(
    { name: optional(string) },
    ({ name }) => name || `${idCounter.count++}`,
  ),
  props: optional(
    array(
      record({
        name: string,
        bind: record({
          type: union(literal('expr'), literal('sync')),
          ref: string,
        }),
      }),
    ),
  ),
  layout: optional((pojo) => {
    if (typeof pojo !== 'object' || Array.isArray(pojo)) {
      throw new Error('expect an object');
    }
    return pojo;
  }),
  on: optional(
    array(
      record({
        name: string,
        code: string,
      }),
    ),
  ),
  elements: optional(array((pojo) => validateViewTemplateData(pojo))),
});

function createViewModelBinding<VM>(
  binding: ModelBinding,
): ViewModelBinding<VM> {
  if (binding.type === 'expr') {
    return new Function('vm', `return ${binding.ref}`) as any;
  }
  return (vm) => get(vm, binding.ref);
}

function createViewEventBinding<VM>(code: string): ViewEventBinding<VM> {
  return new Function('args', 'vm', 'ctx', code) as any;
}

export function createTemplateFromSchema(
  schema: ViewTemplateSchema,
): ViewTemplate<GeneralModel> {
  const props = schema.props?.reduce((m, prop) => {
    m[prop.name] = createViewModelBinding(prop.bind);
    return m;
  }, {} as Record<string, ViewModelBinding<GeneralModel>>);
  const on = schema.on?.reduce((m, evt) => {
    m[evt.name] = createViewEventBinding(evt.code);
    return m;
  }, {} as Record<string, ViewEventBinding<GeneralModel>>);
  return {
    type: schema.type,
    name: schema.name,
    props,
    on,
    elements: schema.elements?.map(createTemplateFromSchema),
  };
}

export function compileTemplateData(json: unknown) {
  return createTemplateFromSchema(validateViewTemplateData(json as any));
}
